Drupal 8 study note 7 : Plug and Play with Plugins-2 Creating a custom plugin type

The plugin system provides means to create specialized objects in Drupal that do not require the robust features of the entity system.

In this recipe, we will create a new plugin type called Unit that will work with units of measurement and conversions. We will create a plugin manager, default plugin interface, YAML discovery method, base class, and plugin definition.

This recipe is based on the work being done to export the Physical module to Drupal 8. The Physical module provides a way to work with units of volume, weight, and dimensions and attaches them to entities. It discovers unit plugins in the same way that the Breakpoint module discovers breakpoint plugins.

  1. All plugins need to have a service that acts as a plugin manager. Create a new file in your module’s src directory called UnitManager.php. This will hold the UnitManager class.
  2. Create the UnitManager class by extending the \Drupal\Core\Plugin\ DefaultPluginManager class:

    <?php

    /**

    * @file

    * Contains \Drupal\mymodule\UnitManager.

    */

    namespace Drupal\mymodule;

    use Drupal\Core\Plugin\DefaultPluginManager;

    use Drupal\Core\Cache\CacheBackendInterface;

    use Drupal\Core\Extension\ModuleHandlerInterface;

    class UnitManager extends DefaultPluginManager {

    }

  3. When creating a new plugin type, it is recommended that the plugin manager provides a set of defaults for new plugins, if an item is missing. This is also useful to define the default class a plugin should use:

    <?php

    /**

    * @file

    * Contains \Drupal\mymodule\UnitManager.

    */

    namespace Drupal\mymodule;

    use Drupal\Core\Plugin\DefaultPluginManager;

    use Drupal\Core\Cache\CacheBackendInterface;

    use Drupal\Core\Extension\ModuleHandlerInterface;

    class UnitManager extends DefaultPluginManager {

    /**

    * Default values for each unit plugin.

    *

    * @var array

    */

    protected $defaults = [

    ‘id’ => ”,

    ‘label’ => ”,

    ‘unit’ => ”,

    ‘factor’ => 0.00,

    ‘type’ => ”,

    ‘class’ => ‘Drupal\mymodule\Unit’,

    ];

    }

  4. Later, we will create the Unit class in our module that unit plugins will be instances of.

  5. class constructor to define the module handler and cache backend:

    <?php

    /**

    * @file

    * Contains \Drupal\mymodule\UnitManager.

    */

    use Drupal\Core\Plugin\DefaultPluginManager;

    use Drupal\Core\Cache\CacheBackendInterface;

    use Drupal\Core\Extension\ModuleHandlerInterface;

    class UnitManager extends DefaultPluginManager {

    /**

    * Default values for each unit plugin.

    *

    * @var array

    */

    protected $defaults = [

    ‘id’ => ”,

    ‘label’ => ”,

    ‘unit’ => ”,

    ‘factor’ => 0.00,

    ‘type’ => ”,

    ‘class’ => ‘Drupal\physical\Unit’,

    ];

    /**

    * Constructs a new \Drupal\mymodule\UnitManager object.

    *

    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_ backend

    * Cache backend instance to use.

    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_ handler

    * The module handler to invoke the alter hook with.

    */

    public function __construct(CacheBackendInterface $cache_ backend, ModuleHandlerInterface $module_handler) {

    $this->moduleHandler = $module_handler;

    $this->setCacheBackend($cache_backend, ‘physical_unit_ plugins’);

    }

    }

  6. We override the constructor so that we can specify a specific cache key. This allows plugin definitions to be cached and cleared properly; otherwise, our plugin manager will continuously read the disk to find plugins.

  7. We also need to override the getDiscovery method. We need to implement a YAML discovery method:

    <?php

    /**

    * @file

    * Contains \Drupal\mymodule\UnitManager.

    */

    namespace Drupal\mymodule;

    use Drupal\Core\Plugin\DefaultPluginManager;

    use Drupal\Core\Cache\CacheBackendInterface;

    use Drupal\Core\Extension\ModuleHandlerInterface;

    class UnitManager extends DefaultPluginManager {

    /**

    * Default values for each unit plugin.

    *

    * @var array

    */

    protected $defaults = [

    ‘id’ => ”,

    ‘label’ => ”,

    ‘unit’ => ”,

    ‘factor’ => 0.00,

    ‘type’ => ”,

    ‘class’ => ‘Drupal\mymodule\Unit’,

    ];

    /**

    * Constructs a new \Drupal\mymodule\UnitManager object.

    *

    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_ backend

    * Cache backend instance to use.

    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_ handler

    * The module handler to invoke the alter hook with.

    */

    public function __construct(CacheBackendInterface $cache_ backend, ModuleHandlerInterface $module_handler) {

    $this->moduleHandler = $module_handler;

    $this->setCacheBackend($cache_backend, ‘physical_unit_ plugins’);

    }

    /**

    * {@inheritdoc}

    */

    protected function getDiscovery() {

    if (!isset($this->discovery)) {

    $this->discovery = new YamlDiscovery(‘units’, $this- >moduleHandler->getModuleDirectories());

    $this->discovery = new ContainerDerivativeDiscoveryDecorator ($this->discovery);

    }

    return $this->discovery;

    }

    }

  8. The default plugin manager implementation supports an annotated plugin discovery, such as field types, field widgets, and field formatters. By setting the discovery property to YamlDiscovery, we are telling Drupal to look for a *.units.yml file in all the module directories.

  9. The next step is to create a mymodule.services.yml in your module’s directory. This will describe our plugin manager to Drupal, allowing a plugin discovery:

    services:

    plugin.manager.unit:

    class: Drupal\mymodule\UnitManager

    arguments: [‘@container.namespaces’, ‘@cache.discovery’, ‘@ module_handler’]

  10. Drupal utilizes services and dependency injection. By defining our class as a service, we are telling the application container how to initiate our class. This will allow us to retrieve the manager and access plugins even if another module replaces our defined plugin manager.

  11. Next, we will define the plugin interface that we defined in the plugin manager. The plugin manager will validate the Unit plugins that implement this interface. Create a UnitInterface.php file in your module’s src directory to hold the interface:

    <?php

    /**

    * @file

    * Contains \Drupal\mymodule\UnitInterface.

    */

    namespace Drupal\mymodule;

    /**

    * Interface UnitInterface.

    */

    interface UnitInterface {

    /**

    * Returns the unit’s label.

    *

    * @return string

    * The unit’s label.

    */

    public function getLabel();

    /**

    * Returns the unit abbreviation.

    *

    * @return string

    * The abbreviation.

    */

    public function getUnit();

    /**

    * Returns the factor amount for conversions.

    *

    * @return int|float

    * The factor amount.

    */

    public function getFactor();

    /**

    * Converts a value to the base unit.

    *

    * @param int|float $value

    * The amount to convert.

    *

    * @return int|float

    * The converted amount.

    */

    public function toBase($value);

    /**

    * Converts value from base unit to current unit.

    *

    * @param int|float $value

    * The amount to convert.

    *

    * @return int|float

    * The converted amount.

    */

    public function fromBase($value);

    /**

    * Rounds a value.

    *

    * @param int|float $value

    * The value to round.

    *

    * @return int|float

    * The rounded value.

    */

    public function round($value);

    }

  12. We provide an interface so that we can guarantee that we have these expected methods when working with a Unit plugin and have an output, regardless of the logic behind each method. It pushes for encapsulation when working with plugins.

  13. Create a mymodule.units.yml file to provide default unit plugin definitions:

    centimeters:

    label: Centimeters

    unit: cm

    factor: 1E-2

    type: dimensions

    meters:

    label: Meters

    unit: m

    factor: 1

    type: dimensions

    feet:

    label: Feet

    unit: ft

    factor: 3.048E-1

    type: dimensions

    inches:

    label: Inches

    unit: in

    factor: 2.54E-2

    type: dimensions

  14. As defined in our plugin’s default definition, we need to provide a Unit class. Create Unit.php in your module’s src directory. This class will implement our UnitInterface interface:

    <?php

    /**

    * @file

    * Contains \Drupal\mymodule\Unit.

    */

    namespace Drupal\mymodule;

    use Drupal\Core\Plugin\PluginBase;

    /**

    * Class Unit.

    */

    class Unit extends PluginBase implements UnitInterface {

    /**

    * {@inheritdoc}

    */

    public function getFactor() {

    return (float) $this->pluginDefinition[‘factor’];

    }

    /**

    * {@inheritdoc}

    */

    public function getLabel() {

    return $this->t($this->pluginDefinition[‘label’], array(), array(‘context’ => ‘unit’));

    }

    /**

    * {@inheritdoc}

    */

    public function getUnit() {

    }

    /**

    * {@inheritdoc}

    */

    public function toBase($value) {

    return $this->round($value * $this->getFactor());

    }

    /**

    * {@inheritdoc}

    */

    public function fromBase($value) {

    return $this->round($value / $this->getFactor());

    }

    /**

    * {@inheritdoc}

    */

    public function round($value) {

    return round($value, 5);

    }

    /**

    * Returns the unit’s label.

    *

    * @return string

    * Unit label.

    */

    public function __toString() {

    return $this->getLabel();

    }

    }

  15. This class implements all the required methods defined in our interface. The toBase and fromBase methods allow us to convert the unit’s value from its defined factor value.

  16. The Unit plugin is now implemented and can be integrated through a custom field type or another custom code.

  17. An example usage would be to create a custom form that allows users to convert values. The following code can be placed in the submit method and will allow us to load our plugin for feet and return the value in meters:

    // Load the manager service.

    $unit_manager = \Drupal::service(‘plugin.manager.unit’);

    // Create a class instance through the manager.

    $feet_instance = $unit_manager->createInstance(‘feet’);

    // Convert 12ft into meters.

    $meters_value = $feet_instance->toBase(12);

 

 

Leave a Comment